﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;

/**
 * (*1) 仕様書にはGetRefferences()にバインド対象のオブジェクトを
 * 含めるか否かの記述はない(ので含めるのが正しいと思われる)。
 * ただしそれだと参照が循環してしまうのでこの実装では含めていない。
 * 
 * */

namespace M3G {
    public class ShaderUniforms : Object3D {
        #region Constant
        // バインド対象のターゲット・プロパティ
        // (注意)使用しない!!
        // 任意のプロパティをバインドできるようにプロパティ名を文字列で渡す
        public const int AlphaFactor          = 384;
        public const int BoneMatrices         = 385;
        public const int CameraFarDistance    = 386;
        public const int CameraFieldOfView    = 387;
        public const int CameraNearDistance   = 388;
        public const int CameraProjection     = 389;
        public const int CameraSpaceDirection = 390;
        public const int CameraSpacePosition  = 391;
        public const int CompositeMatrix      = 392;
        public const int LightColor           = 393;
        public const int Matrix               = 394;
        public const int ModelSpaceDirection  = 395;
        public const int ModelSpacePosition   = 396;
        public const int Orientation          = 397;
        public const int Scale                = 398;
        public const int Translation          = 399;
        public const int WorldSpaceDirectino  = 400;
        public const int WorldSpacePosition   = 401;
        static public readonly Node CurrentCamera = new Camera ();
        static public readonly Node CurrentMesh   = new Mesh (1, 0);
        #endregion

        struct ObjectBinding {
            public string name;
            public Object3D[] sources;
        }
        struct TranformBinding {
            public Object3D[] src;
            public Object3D[] dst;
        }

        #region Field
        List<ShaderVariable>                 shaderVariables;
        Dictionary<int, ObjectBinding>   objectBindings;
        Dictionary<int, TranformBinding> transformBindings;
        #endregion

        #region Method
        public ShaderUniforms () {
            this.shaderVariables = new List<ShaderVariable> ();
            this.objectBindings = new Dictionary<int, ObjectBinding> ();
            this.transformBindings = new Dictionary<int, TranformBinding> ();
        }

        protected ShaderUniforms (ShaderUniforms unifs)
            : base (unifs) {
            this.shaderVariables = new List<ShaderVariable> (unifs.shaderVariables);
            this.objectBindings = new Dictionary<int, ObjectBinding> (unifs.objectBindings);
            this.transformBindings = new Dictionary<int, TranformBinding> (unifs.transformBindings);
        }



        public void AddAnimationTrack (int id, AnimationTrack animationTrack, int channel) {
            if (animationTrack == null) {
                throw new ArgumentException ("AnimationTrack is null");
            }
            if (id < 0 || id > UniformCount) {
                throw new ArgumentException ("Invalid ID, it must be within [0," + UniformCount + "]");
            }
            for (var i=0; i < AnimationTrackCount; i++) {
                if (AnimationTrackUniform[i] == id && AnimationTrackChannel[i] == channel) {
                    throw new ArgumentException ("Specified AnimationTrack and channel is already attached to specified Uniform ID");
                }
            }
            if (shaderVariables[id].ArraySize != 0) {
                throw new ArgumentException ("Uniform must be a scaler type (ArrayLength=0), len=" + shaderVariables[id].ArraySize);
            }
            if ((int)shaderVariables[id].Type != animationTrack.TargetProperty) {
                throw new ArgumentException ("Target property (basicType) of specified AnimationTrack must be equal to a type of specified UniformVariable");
            }
            var channelCount = animationTrack.KeyframeSequence.ChanelCount;
            if (channel < 0 || channel >= channelCount) {
                throw new ArgumentException ("Channel index must be within [0," + channelCount + "]");
            }

            AddAnimationTrackUniform (animationTrack, channel, id);
        }

        public void AddAnimationTrack (int id, AnimationTrack animationTrack) {
            if (animationTrack == null) {
                throw new ArgumentException ("AnimationTrack is null");
            }
            if (id < 0 || id > UniformCount) {
                throw new ArgumentException ("Invalid ID, it must be within [0," + (UniformCount - 1) + "]");
            }
            for (var i=0; i < AnimationTrackCount; i++) {
                if (AnimationTrackUniform[i] == id && AnimationTrack[i] == animationTrack) {
                    throw new ArgumentException ("Specified animationTrack is already attached to specified Uniform ID");
                }
            }
            if ((int)shaderVariables[id].Type != animationTrack.TargetProperty) {
                throw new ArgumentException ("Target property(basicType) of given animationTrack must be equal with a type of given ShaderVariable");
            }
            var channelCount = animationTrack.KeyframeSequence.ChanelCount;
            var required     = shaderVariables[id].ArraySize;
            if (channelCount < required) {
                throw new ArgumentException ("Channel count of specified AnimationTrack is less than required, ch=" + channelCount + ",req=" + shaderVariables[id].ArraySize);
            }

            base.AddAnimationTrackUniform (animationTrack, 0, id);
        }


        /**
         * メモ:スカラーの時はarrayLength=0を指定する
         * */
        public int AddUniform (string name, ShaderVariableType basicType, int arrayLength) {
            if (name == null) {
                throw new ArgumentNullException ("Name is null");
            }
            if (basicType < ShaderVariableType.Float && basicType > ShaderVariableType.SamplerCube) {
                throw new ArgumentException ("Type is invalid, type=" + basicType);
            }
            if (name == "") {
                throw new ArgumentException ("Name is empty");
            }
            if (name.StartsWith ("m3g_", true, null) && name.StartsWith ("_m3g", true, null)) {
                throw new ArgumentException ("Don't use reserved word(\"m3g_\",\"_m3g\"),name=" + name);
            }
            if (arrayLength < 0) {
                throw new ArgumentException ("ArrayLength must be positive, len=" + arrayLength);
            }

            shaderVariables.Add (new ShaderVariable (name, basicType, arrayLength, 0));

            return GetUniformID (name);
        }

        public int GetUniformID (string name) {
            if (name == null) {
                throw new ArgumentNullException ("Name is null");
            }
            if (name.Length == 0) {
                throw new ArgumentException ("Name is empty");
            }

            return shaderVariables.IndexOf (x => x.Name == name);
        }

        public void SetTransformBinding (int id, Node from, Node to) {

            var info = new TranformBinding ();
            info.src = new Node[] { from };
            info.dst = new Node[] { to };
            this.transformBindings.Add (id, info);
        }

        public void SetTransformBinding (int id, Node[] from, Node to) {

            var info = new TranformBinding ();
            info.src = (Node[])from.Clone ();
            info.dst = new Node[] { to };
            this.transformBindings.Add (id, info);
        }

        public void SetTransformBinding (int id, Node from, Node[] to) {
            var info = new TranformBinding ();
            info.src = new Node[] { from };
            info.dst = (Node[])to.Clone ();
            this.transformBindings.Add (id, info);
        }

        public void SetObjectBinding (int id, Object3D source, string name) {
            if (id < 0 || id >= UniformCount) {
                throw new ArgumentException ("Specified ID is invalid, id=" + id);
            }
            var info = new ObjectBinding ();
            info.sources = new Object3D[] { source };
            info.name = name;
            this.objectBindings.Add (id, info);
        }

        public void SetObjectBinding (int id, Object3D[] sources, string name) {
            var required = shaderVariables[id].ArraySize;
            if (sources != null && sources.Length < required) {
                throw new ArgumentException ("Length of source is less than required, len=" + sources.Length + ",req=" + required);
            }
            if (id < 0 || id >= UniformCount) {
                throw new ArgumentException ("Specified ID is invalid, id=" + id);
            }

            var info = new ObjectBinding ();
            info.sources = (Object3D[])sources.Clone ();
            info.name = name;
            this.objectBindings.Add (id, info);
        }

        public int GetTransformBinding (int id, out Node[] from, out Node[] to) {
            if (id < 0 || id >= UniformCount) {
                throw new ArgumentException ("Invalid ID, it must be within [0," + UniformCount + "]");
            }
            if (!transformBindings.ContainsKey (id)) {
                throw new ArgumentException ("Specified ID is not binded to Transform");
            }

            from = (Node[])transformBindings[id].src.Clone ();
            to = (Node[])transformBindings[id].dst.Clone ();

            return from.Length;
        }

        public string GetObjectBinding (int id, out Object3D[] sources) {
            if (id < 0 || id >= UniformCount) {
                throw new ArgumentException ("Invalid ID, it must be within [0," + UniformCount + "]");
            }
            if (!objectBindings.ContainsKey (id)) {
                throw new ArgumentException ("Specified ID is not binded to Object3D");
            }

            sources = (Object3D[])objectBindings[id].sources.Clone ();
            return objectBindings[id].name;
        }



        public int UniformCount {
            get { return shaderVariables.Count (); }
        }

        public ShaderVariable GetUniform (int id) {
            return shaderVariables[id];
        }


        /**
         * (*1)
         * 仕様書ではスカラー値の時も配列で返すことになっているが
         * スカラー値で返した方が使いやすそうなのでわざとこのように実装している
         * 将来的に仕様書通りの実装に戻すことはあり得る
         * */
        public object GetUniformValue (int id) {
            var type      = shaderVariables[id].Type;
            var values    = shaderVariables[id].Values;
            var arraySize = shaderVariables[id].ArraySize;
            switch (type) {
                case ShaderVariableType.Float: return (arraySize == 0) ? values[0] : values.Cast<float>().ToArray();   // ...(1)
                case ShaderVariableType.Bool: return (arraySize == 0) ? values[0] : values.Cast<bool> ().ToArray ();   // ...(1)
                case ShaderVariableType.Int: return (arraySize == 0) ? values[0] : values.Cast<int> ().ToArray ();   // ...(1)
                case ShaderVariableType.Vec2: return values.Cast<float> ().ToArray();
                case ShaderVariableType.Vec3: return values.Cast<float> ().ToArray ();
                case ShaderVariableType.Vec4: return values.Cast<float> ().ToArray ();
                case ShaderVariableType.BVec2: return values.Cast<bool> ().ToArray ();
                case ShaderVariableType.BVec3: return values.Cast<bool> ().ToArray ();
                case ShaderVariableType.BVec4: return values.Cast<bool> ().ToArray ();
                case ShaderVariableType.IVec2: return values.Cast<int> ().ToArray ();
                case ShaderVariableType.IVec3: return values.Cast<int> ().ToArray ();
                case ShaderVariableType.IVec4: return values.Cast<int> ().ToArray ();
                case ShaderVariableType.Mat2: return values.Cast<Transform> ().ToArray ();
                case ShaderVariableType.Mat3: return values.Cast<Transform> ().ToArray ();
                case ShaderVariableType.Mat3x4: return values.Cast<Transform> ().ToArray ();
                case ShaderVariableType.Mat4: return values.Cast<Transform> ().ToArray ();
                case ShaderVariableType.Sampler2D: return values.Cast<Texture> ().ToArray ();
                case ShaderVariableType.SamplerCube: return values.Cast<Texture> ().ToArray ();
                default: return null;
            }
        }

        public IEnumerable<ShaderVariable> AllShaderVariables {
            get { return shaderVariables; }
        }


        public void SetUniformValue (int id, bool x) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Uniform ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Bool) {
                throw new ArgumentException ("Shader type must be Bool, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
        }

        public void SetUniformValue (int id, bool x, bool y) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.BVec2) {
                throw new ArgumentException ("Shader type must be BVec2, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
            this.shaderVariables[id].SetValue (1, y);
        }

        public void SetUniformValue (int id, bool x, bool y, bool z) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.BVec3) {
                throw new ArgumentException ("Shader type must be BVec3, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
            this.shaderVariables[id].SetValue (1, y);
            this.shaderVariables[id].SetValue (2, z);
        }

        public void SetUniformValue (int id, bool x, bool y, bool z, bool w) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.BVec4) {
                throw new ArgumentException ("Shader type must be BVec4, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
            this.shaderVariables[id].SetValue (1, y);
            this.shaderVariables[id].SetValue (2, z);
            this.shaderVariables[id].SetValue (3, w);
        }

        public void SetUniformValue (int id, float x) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Float) {
                throw new ArgumentException ("Shader type must be Float, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
        }

        public void SetUniformValue (int id, float x, float y) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Vec2) {
                throw new ArgumentException ("Shader type must be Vec2, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
            this.shaderVariables[id].SetValue (1, y);
        }

        public void SetUniformValue (int id, float x, float y, float z) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Vec3) {
                throw new ArgumentException ("Shader type must be Vec3, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
            this.shaderVariables[id].SetValue (1, y);
            this.shaderVariables[id].SetValue (2, z);
        }

        public void SetUniformValue (int id, float x, float y, float z, float w) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Vec4) {
                throw new ArgumentException ("Shader type must be Vec4, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
            this.shaderVariables[id].SetValue (1, y);
            this.shaderVariables[id].SetValue (2, z);
            this.shaderVariables[id].SetValue (3, w);
        }

        public void SetUniformValue (int id, int x) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Int) {
                throw new ArgumentException ("Shader type must be Int, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
        }

        public void SetUniformValue (int id, int x, int y) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.IVec2) {
                throw new ArgumentException ("Shader type must be IVec2, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
            this.shaderVariables[id].SetValue (1, y);
        }

        public void SetUniformValue (int id, int x, int y, int z) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.IVec3) {
                throw new ArgumentException ("Shader type must be IVec3, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
            this.shaderVariables[id].SetValue (1, y);
            this.shaderVariables[id].SetValue (2, z);
        }

        public void SetUniformValue (int id, int x, int y, int z, int w) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.IVec4) {
                throw new ArgumentException ("Shader type must be IVec4, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, x);
            this.shaderVariables[id].SetValue (1, y);
            this.shaderVariables[id].SetValue (2, z);
            this.shaderVariables[id].SetValue (3, w);
        }

        /**
         * Mat4以外の呼び出しはバグってる。直せ > 俺
         * */
        public void SetUniformValue (int id, Transform matrix) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Mat3 && shaderVariables[id].Type != ShaderVariableType.Mat3x4 && shaderVariables[id].Type != ShaderVariableType.Mat4) {
                throw new ArgumentException ("Shader type must be Mat3,Mat3x4,Mat4, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, matrix);
        }

        public void SetUniformValue (int id, Texture sampler) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Sampler2D && shaderVariables[id].Type != ShaderVariableType.SamplerCube) {
                throw new ArgumentException ("Shader type must be Sampler2D,SamplerCube, type=" + shaderVariables[id].Type);
            }

            this.shaderVariables[id].SetValue (0, sampler);
        }


        public void SetUniformValueArray (int id, bool[] values) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Bool &&
                shaderVariables[id].Type != ShaderVariableType.BVec2 &&
                shaderVariables[id].Type != ShaderVariableType.BVec3 &&
                shaderVariables[id].Type != ShaderVariableType.BVec4) {
                throw new ArgumentException ("Shader type must be Bool,BVec2,BVec3,BVec4, type=" + shaderVariables[id].Type);
            }
            var type      = shaderVariables[id].Type;
            var arraySize = shaderVariables[id].ArraySize;

            var required = type.ToElementCount () * arraySize;
            if (values.Length< required) {
                throw new ArgumentException ("Values are less than required, len=" + values.Length + ", req=" + required);
            }

            for (var i=0; i < required; i++) {
                this.shaderVariables[id].SetValue (i, values[i]);
            }
        }

        public void SetUniformValueArray (int id, int[] values) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Int &&
                shaderVariables[id].Type != ShaderVariableType.IVec2 &&
                shaderVariables[id].Type != ShaderVariableType.IVec3 &&
                shaderVariables[id].Type != ShaderVariableType.IVec4) {
                throw new ArgumentException ("Shader type must be Int,IVec2,IVec3,IVec4, type=" + shaderVariables[id].Type);
            }
            var type      = shaderVariables[id].Type;
            var arraySize = shaderVariables[id].ArraySize;

            var required = type.ToElementCount () * arraySize;
            if (values.Length < required) {
                throw new ArgumentException ("Values are less than required, len=" + values.Length + ", req=" + required);
            }

            for (var i=0; i < required; i++) {
                this.shaderVariables[id].SetValue (i, values[i]);
            }
        }

        public void SetUniformValueArray (int id, float[] values) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Float &&
                shaderVariables[id].Type != ShaderVariableType.Vec2 &&
                shaderVariables[id].Type != ShaderVariableType.Vec3 &&
                shaderVariables[id].Type != ShaderVariableType.Vec4 &&
                shaderVariables[id].Type != ShaderVariableType.Mat2 &&
                shaderVariables[id].Type != ShaderVariableType.Mat3 &&
                shaderVariables[id].Type != ShaderVariableType.Mat4 &&
                shaderVariables[id].Type != ShaderVariableType.Mat3x4) {
                throw new ArgumentException ("Shader type must be Float,Vec2,Vec3,Vec4,Mat2,Mat3,Mat4,Mat3x4, type=" + shaderVariables[id].Type);
            }
            var type      = shaderVariables[id].Type;
            var arraySize = shaderVariables[id].ArraySize;

            var required = type.ToElementCount () * arraySize;
            if (values.Length < required) {
                throw new ArgumentException ("Values are less than required, len=" + values.Length + ", req=" + required);
            }

            for (var i=0; i < required; i++) {
                this.shaderVariables[id].SetValue (i, values[i]);
            }
        }


        public void SetUniformValue (int id, Texture[] samplers) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Sampler2D &&
                shaderVariables[id].Type != ShaderVariableType.SamplerCube) {
                throw new ArgumentException ("Shader type must be Sampler2D,SamplerCube, type=" + shaderVariables[id].Type);
            }
            var type      = shaderVariables[id].Type;
            var arraySize = shaderVariables[id].ArraySize;

            var required = type.ToElementCount () * arraySize;
            if (samplers.Length < required) {
                throw new ArgumentException ("Values are less than required, len=" + samplers.Length + ", req=" + required);
            }

            for (var i=0; i < required; i++) {
                this.shaderVariables[id].SetValue (i, samplers[i]);
            }
        }

        public void SetUniformValueArray (int id, Transform[] matrices) {
            if (id < 0 && id >= UniformCount) {
                throw new ArgumentException ("Shader ID doesn't exist, id=" + id);
            }
            if (shaderVariables[id].Type != ShaderVariableType.Mat3 &&
                shaderVariables[id].Type != ShaderVariableType.Mat4 &&
                shaderVariables[id].Type != ShaderVariableType.Mat3x4) {
                throw new ArgumentException ("Shader type must be Mat3,Mat4,Mat3x4, type=" + shaderVariables[id].Type);

            }
            var type      = shaderVariables[id].Type;
            var arraySize = shaderVariables[id].ArraySize;

            var required = type.ToElementCount () * arraySize;
            if (matrices.Length < required) {
                throw new ArgumentException ("Values are less than required, len=" + matrices.Length + ", req=" + required);
            }

            for (var i=0; i < required; i++) {
                this.shaderVariables[id].SetValue (i, matrices[i]);
            }
        }

        public override Object3D[] References {
            get {
                var q = new List<Object3D> ();
                q.AddRange (base.References);
                // (*1) see comment.
                //foreach (var src in objectBindings) {
                //    q.AddRange (src.Value.src);
                //}
                //foreach (var src in transformBindings) {
                //    q.AddRange (src.Value.src);
                //    q.AddRange (src.Value.dst);
                //}
                q.RemoveAll (x => x == null);
                return q.ToArray ();
            }
        }

        public void Refresh () {
            foreach (var bind in objectBindings) {
                var id   = bind.Key;
                var name = bind.Value.name;
                var obj  = bind.Value.sources[0];   // 現在はスカラー(Length=0)のみ対応
                var type = obj.GetType ();
                var prop = type.GetProperty (name);
                var value = prop.GetValue (obj, null);

                shaderVariables[id].SetValue (0, value);
            }
            //foreach (var src in transformBindings) {
            //    q.AddRange (src.Value.src);
            //    q.AddRange (src.Value.dst);
            //}
        }

        public override Object3D Duplicate () {
            return new ShaderUniforms (this);
        }

        public static string GetName<T> (Expression<Func<T>> e) {
            var member = (MemberExpression)e.Body;
            return member.Member.Name;
        }
        #endregion
    }
}